import configparser
import requests
import sys
import os
import re
import time
import traceback
from bs4 import BeautifulSoup
from tqdm import tqdm
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from concurrent.futures import ThreadPoolExecutor, as_completed
import logging


# -------------------------- 基础初始化（日志、配置） --------------------------
def init_logger():
    """初始化日志系统：同时输出到控制台和日志文件"""
    log_format = "%(asctime)s - %(levelname)s - %(message)s"
    logging.basicConfig(
        level=logging.INFO,
        format=log_format,
        handlers=[
            logging.FileHandler("music_downloader.log", encoding="utf-8"),
            logging.StreamHandler()
        ]
    )
    return logging.getLogger(__name__)


logger = init_logger()  # 全局日志对象


def create_config(ms=1):
    if ms == 1:
        """首次运行时自动生成配置文件，引导用户设置默认参数"""
        print("\n" + "="*80)
        print("          未检测到配置文件，开始引导设置")
        print("="*80)
    elif ms == 2:
        print('重置配置文件：')
        next   
    # 1. 歌曲保存路径
    default_song_path = os.path.abspath(".")
    song_path = input(f"1. 歌曲默认保存路径（直接回车用：{default_song_path}）：").strip()
    song_path = song_path if song_path else default_song_path
    
    # 2. 歌词保存路径
    default_lyrics_path = os.path.abspath(".")
    lyrics_path = input(f"2. 歌词默认保存路径（直接回车用：{default_lyrics_path}）：").strip()
    lyrics_path = lyrics_path if lyrics_path else default_lyrics_path
    
    # 3. 默认是否下载歌词
    lrc_default = input("3. 是否默认下载歌词？(y/n，直接回车默认n)：").strip().lower()
    lrc_default = lrc_default if lrc_default in ["y", "n"] else "n"
    
    # 4. 是否按歌手分类保存歌曲（新增：按配置开关控制）
    artist_folder = input("4. 是否按歌手分类保存歌曲？(y/n，直接回车默认n)：").strip().lower()
    artist_folder = artist_folder if artist_folder in ["y", "n"] else "n"
    
    # 5. 最大下载线程数（多线程控制）
    thread_count = input("5. 批量下载最大线程数（1-5，直接回车默认3）：").strip()
    thread_count = int(thread_count) if thread_count.isdigit() and 1 <= int(thread_count) <= 5 else 3
    
    # 生成配置文件
    config = configparser.ConfigParser()
    config["Paths"] = {
        "song_save_path": song_path,
        "lyrics_save_path": lyrics_path,
        "lrc_download": lrc_default,
        "enable_artist_folder": artist_folder,  # 歌手分类开关
        "max_download_threads": str(thread_count)  # 最大线程数
    }
    
    with open("config.ini", "w", encoding="utf-8") as f:
        config.write(f)
    
    logger.info(f"配置文件已生成：{os.path.abspath('config.ini')}")
    print(f"\n✅ 配置文件已生成，路径：{os.path.abspath('config.ini')}")
    input("按回车键继续...")


def load_config(config_file="config.ini"):
    """加载配置文件，无配置文件时自动触发创建"""
    if not os.path.exists(config_file):
        create_config()
    
    config = configparser.ConfigParser()
    try:
        config.read(config_file, encoding="utf-8")
        # 读取基础配置（带默认值）
        return {
            "song_save_path": config.get("Paths", "song_save_path", fallback="."),
            "lyrics_save_path": config.get("Paths", "lyrics_save_path", fallback="."),
            "lrc_default": config.get("Paths", "lrc_download", fallback="n"),
            "enable_artist_folder": config.get("Paths", "enable_artist_folder", fallback="n"),
            "max_threads": int(config.get("Paths", "max_download_threads", fallback="3"))
        }
    except configparser.Error as e:
        logger.error(f"配置文件解析错误：{str(e)}，将使用默认配置")
        print(f"\n⚠️  配置文件解析错误：{str(e)}，将使用默认配置")
        return {
            "song_save_path": ".",
            "lyrics_save_path": ".",
            "lrc_default": "n",
            "enable_artist_folder": "n",
            "max_threads": 3
        }


# 全局配置对象（程序启动时加载）
GLOBAL_CONFIG = load_config()


# -------------------------- 网络请求工具（带重试） --------------------------
@retry(
    stop=stop_after_attempt(3),  # 最多重试3次
    wait=wait_exponential(multiplier=1, min=2, max=10),  # 重试间隔：2s→4s→8s
    retry=retry_if_exception_type((requests.exceptions.Timeout, requests.exceptions.ConnectionError))
)
def requests_get_with_retry(url, headers, timeout=10):
    """带重试机制的GET请求（仅对超时/连接错误重试）"""
    response = requests.get(url, headers=headers, timeout=timeout)
    response.raise_for_status()  # 4xx/5xx状态码抛异常
    response.encoding = "utf-8"
    return response.text


@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=2, max=10),
    retry=retry_if_exception_type((requests.exceptions.Timeout, requests.exceptions.ConnectionError))
)
def requests_post_with_retry(url, headers, data, timeout=10):
    """带重试机制的POST请求"""
    response = requests.post(url, headers=headers, data=data, timeout=timeout)
    response.raise_for_status()
    return response.json()


def get_url_html(url):
    """获取网页HTML内容（基于重试请求）"""
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
        "Authorization": "Bearer your_token",
        "Content-Type": "application/json"
    }
    try:
        return requests_get_with_retry(url, headers)
    except requests.exceptions.RequestException as e:
        logger.error(f"网页请求失败（URL：{url}）：{str(e)}", exc_info=True)
        print(f"\n❌ 网页请求失败：{str(e)}")
        return None


# -------------------------- 歌曲信息解析工具 --------------------------
def title():
    """打印程序标题"""
    print('')
    print(r'------------------------------------')
    print(r'   ____   ___   ____   ____   _     ')
    print(r'  / ___| / _ \ | __ ) |  _ \ | |    ')
    print(r' | |  _ | | | ||  _ \ | | | || |    ')
    print(r' | |_| || |_| || |_) || |_| || |___ ')
    print(r'  \____| \__\_\|____/ |____/ |_____|')
    print(r'         音乐下载工具（V2.1.0）      ')
    print(r'------------------------------------')


def get_song_names():
    while True:
        """获取歌曲列表（支持手动输入/文件导入）"""
        clear()
        title()
        print("\n📥 请选择：")
        print("1. 手动输入（多个歌曲用 /\\ 分隔，例：晴天/\\七里香）")
        print("2. 从TXT文件导入（每行1首歌曲名，例：./songlist.txt）")
        print("3. 设置")

        while True:
            choice = input("\n请输入序号：").strip()
            if choice == "1":
                song_input = input("请输入歌曲名：").strip()
                song_names = [name.strip() for name in song_input.split('/\\') if name.strip()]
                if not song_names:
                    print("❌ 未输入有效歌曲名，请重新输入！")
                    continue
                return song_names

            elif choice == "2":
                txt_path = input("请输入TXT文件路径：").strip()
                if not os.path.exists(txt_path):
                    print("❌ 文件不存在，请重新输入！")
                    continue
                if not txt_path.lower().endswith(".txt"):
                    print("❌ 请选择TXT格式文件！")
                    continue
                
                # 读取TXT文件（跳过空行）
                song_names = []
                try:
                    with open(txt_path, "r", encoding="utf-8") as f:
                        song_names = [line.strip() for line in f if line.strip()]
                except Exception as e:
                    logger.error(f"读取TXT文件失败（路径：{txt_path}）：{str(e)}", exc_info=True)
                    print(f"❌ 读取TXT文件失败：{str(e)}，请重新选择文件！")
                    continue
                
                # 读取成功后判断是否有有效歌曲名
                if not song_names:
                    print("❌ TXT文件中无有效歌曲名！")
                    continue
                logger.info(f"从TXT文件成功读取 {len(song_names)} 首歌曲")
                print(f"✅ 成功读取 {len(song_names)} 首歌曲（TXT路径：{txt_path}）")
                input("按回车键继续...")
                return song_names

            elif choice == "3":
                #设置
                clear()
                title()
                print("设置：\n1. 修改配置文件\n2. 返回")            
                user_setting = input('请选择：')
                if user_setting == '1':
                    create_config(ms=2)

                #跳出当前循环，来到第一层循环
                elif user_setting == '2':
                    break
                else:
                    print("❌ 输入错误，请输入正确序号！")

            else:
                print("❌ 输入错误，请输入正确序号！")       


def get_main_url(url, song_name):
    """生成歌曲搜索URL"""
    return f"{url}{song_name}"


def extract_music_info(html_content):
    """解析搜索结果中的歌曲信息（标题、歌手、ID）"""
    if not html_content:
        return []
    soup = BeautifulSoup(html_content, 'html.parser')
    music_list = []
    rows = soup.select('.row .music-link')
    
    for row in rows:
        try:
            title_element = row.select_one('.music-title span')
            title = title_element.text.strip() if title_element else ''
            artist_element = row.select_one('small.text-jade')
            artist = artist_element.text.strip() if artist_element else '未知歌手'
            href = row.get('href', '')
            music_id = href.split('/music/')[-1].split('/')[0] if '/music/' in href else ''
            
            if title and music_id:
                music_list.append({
                    'title': title,
                    'artist': artist,
                    'music_id': music_id
                })
        except Exception as e:
            logger.error(f"解析单首歌曲信息出错：{str(e)}", exc_info=True)
            continue
    
    return music_list


def parse_html(html, model):
    """根据模型解析HTML（1：提取搜索结果区域；2：提取歌曲列表）"""
    if not html:
        return None
    if model == 1:
        pattern = r'<div class="row mb-3">(.*?)<div class="alert alert-warning"'   
        match = re.search(pattern, html, re.DOTALL)    
        return match.group(1).strip() if match else None
    else:
        return extract_music_info(html)


def choose_music(list_music, song_name):
    """让用户选择目标歌曲"""
    clear()
    title()
    print(f"🔍 歌曲《{song_name}》搜索结果（共 {len(list_music)} 首）：")
    for idx, music in enumerate(list_music, 1):
        print(f"[{idx}] 名称：{music['title']} | 歌手：{music['artist']} | ID：{music['music_id']}")
    
    while True:
        user_choose = input('\n请选择歌曲序号：').strip()
        if user_choose.isdigit() and 1 <= int(user_choose) <= len(list_music):
            return user_choose
        print(f"❌ 输入错误，请输入1到{len(list_music)}之间的数字！")


def get_second_url(user_choose, list_music):
    """生成歌曲详情页URL"""
    user_choose_idx = int(user_choose) - 1
    music_info = list_music[user_choose_idx]
    second_url = f'https://www.gequbao.com/music/{music_info["music_id"]}'
    return second_url, music_info


def get_play_id(html):
    """从详情页HTML提取play_id"""
    if not html:
        return None
    pattern = r'"play_id":"(.*?)","mp3_title":"'
    match = re.search(pattern, html)
    return match.group(1) if match else None


def get_download_json(play_id, referer_url):
    """获取下载链接的JSON数据（基于重试POST）"""
    url = "https://www.gequbao.com/api/play-url"
    headers = {
        "authority": "www.gequbao.com",
        "method": "POST",
        "path": "/api/play-url",
        "scheme": "https",
        "accept": "application/json, text/javascript, */*; q=0.01",
        "accept-encoding": "gzip, deflate, br, zstd",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
        "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
        "origin": "https://www.gequbao.com",
        "referer": referer_url,
        "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"Windows\"",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-origin",
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0",
        "x-requested-with": "XMLHttpRequest"
    }
    data = {"id": play_id}
    
    try:
        return requests_post_with_retry(url, headers, data)
    except Exception as e:
        logger.error(f"获取下载JSON失败（play_id：{play_id}）：{str(e)}", exc_info=True)
        print(f"❌ 获取下载信息失败：{str(e)}")
        return None


def get_download_url(json_data):
    """从JSON数据提取下载链接"""
    if json_data and 'data' in json_data and 'url' in json_data['data']:
        return json_data['data']['url']
    logger.warning("下载链接提取失败（JSON格式异常）")
    print("❌ 无法提取下载链接（网站返回格式异常）")
    return None


# -------------------------- 下载工具（含重复检测、多线程） --------------------------
def check_duplicate(save_path):
    """检测重复文件，返回处理策略（覆盖/跳过/重命名）"""
    if not os.path.exists(save_path):
        return "download", save_path
    
    # 计算文件大小（MB）
    file_size = os.path.getsize(save_path) / 1024 / 1024
    clear()
    title()
    print("⚠️  检测到重复文件！")
    print(f"文件路径：{save_path}")
    print(f"文件大小：{file_size:.2f} MB")
    print("\n请选择处理方式：")
    print("1. 覆盖（替换现有文件）")
    print("2. 跳过（不下载该文件）")
    print("3. 重命名（自动添加序号，如“晴天(1).mp3”）")
    
    while True:
        choice = input("\n输入1/2/3：").strip()
        if choice == "1":
            return "overwrite", save_path
        elif choice == "2":
            return "skip", save_path
        elif choice == "3":
            # 生成不重复的文件名
            dirname = os.path.dirname(save_path)
            filename = os.path.basename(save_path)
            name, ext = os.path.splitext(filename)
            count = 1
            new_save_path = os.path.join(dirname, f"{name}({count}){ext}")
            
            while os.path.exists(new_save_path):
                count += 1
                new_save_path = os.path.join(dirname, f"{name}({count}){ext}")
            
            print(f"✅ 自动重命名为：{os.path.basename(new_save_path)}")
            return "rename", new_save_path
        else:
            print("❌ 输入错误，请输入1/2/3！")


def get_save_path(title, artist):
    """生成歌曲/歌词保存路径（支持按歌手分类）"""
    # 清理非法文件名字符（避免Windows路径错误）
    clean_title = re.sub(r'[\/:*?"<>|]', '', title)
    clean_artist = re.sub(r'[\/:*?"<>|]', '', artist)
    
    # 歌曲路径（按配置决定是否加歌手文件夹）
    song_base_path = GLOBAL_CONFIG["song_save_path"]
    if GLOBAL_CONFIG["enable_artist_folder"].lower() == "y":
        song_save_path = os.path.normpath(os.path.join(song_base_path, clean_artist, f"{clean_title}.mp3"))
    else:
        song_save_path = os.path.normpath(os.path.join(song_base_path, f"{clean_title}.mp3"))
    
    # 歌词路径
    lyrics_base_path = GLOBAL_CONFIG["lyrics_save_path"]
    lyrics_save_path = os.path.normpath(os.path.join(lyrics_base_path, f"{clean_title}.lrc"))
    
    return song_save_path, lyrics_save_path


def download_single_song(url, save_path):
    """单首歌曲下载（含进度条、完整性校验）"""
    # 先处理重复文件
    strategy, final_save_path = check_duplicate(save_path)
    if strategy == "skip":
        logger.info(f"跳过下载：{os.path.basename(final_save_path)}（文件已存在）")
        print(f"⏭️  跳过下载：{os.path.basename(final_save_path)}（文件已存在）")
        return True, final_save_path
    elif strategy == "overwrite":
        os.remove(final_save_path)
        logger.info(f"删除旧文件：{os.path.basename(final_save_path)}（准备覆盖）")
    
    # 创建保存目录
    save_dir = os.path.dirname(final_save_path)
    os.makedirs(save_dir, exist_ok=True)
    
    try:
        # 带重试的流式下载
        response = requests.get(url, stream=True, timeout=15)
        response.raise_for_status()
        total_size = int(response.headers.get('content-length', 0))
        
        # 进度条下载
        with tqdm(
            total=total_size,
            unit='B',
            unit_scale=True,
            unit_divisor=1024,
            desc=os.path.basename(final_save_path)
        ) as progress_bar, open(final_save_path, 'wb') as file:
            for chunk in response.iter_content(chunk_size=2048):
                if chunk:
                    file.write(chunk)
                    progress_bar.update(len(chunk))
        
        # 完整性校验
        if total_size > 0 and os.path.getsize(final_save_path) != total_size:
            raise Exception(f"文件大小不匹配（预期：{total_size}B，实际：{os.path.getsize(final_save_path)}B）")
        
        logger.info(f"歌曲下载成功：{final_save_path}")
        print(f"✅ 下载成功：{final_save_path}")
        return True, final_save_path
    
    except Exception as e:
        logger.error(f"歌曲下载失败：{os.path.basename(final_save_path)}，错误：{str(e)}", exc_info=True)
        print(f"❌ 下载失败：{os.path.basename(final_save_path)}，错误：{str(e)}")
        # 清理不完整文件
        if os.path.exists(final_save_path):
            os.remove(final_save_path)
        return False, final_save_path


# -------------------------- 歌词处理工具 --------------------------
def get_lyrics_html(html):
    """从详情页提取歌词HTML片段"""
    if not html:
        return None
    start_tag = '<div class="content-lrc mt-1"'
    start_index = html.find(start_tag)
    if start_index == -1:
        return None
    
    lrc_start = html.find('id="content-lrc">', start_index)
    if lrc_start == -1:
        return None
    lrc_start += len('id="content-lrc">')
    
    end_tag = '<div class="expand" data-status="'
    end_index = html.find(end_tag, lrc_start)
    return html[lrc_start:end_index].strip() if end_index != -1 else None


def process_lrc_text(text):
    """清理歌词文本（去除HTML标签、提取有效时间戳）"""
    if not text:
        return ""
    # 只保留从第一个时间戳开始的内容
    start_index = text.find('[00:00.0]')
    if start_index != -1:
        text = text[start_index:]
    # 清理HTML标签和空格
    return text.replace('<br />', '\n').replace('</div>', '').replace('&nbsp;', ' ').strip()


def validate_lrc(lrc_text):
    """校验歌词是否包含有效时间戳"""
    if not lrc_text:
        return False
    lrc_pattern = r"\[\d{2}:\d{2}\.\d{1,2}\]"
    return bool(re.search(lrc_pattern, lrc_text))


def lrc_download(lrc_text, save_path, temp_opinion):
    """歌词下载（含临时选择、格式校验）"""
    # 先校验歌词有效性
    if not validate_lrc(lrc_text):
        logger.warning(f"歌词无效（无时间戳）：{os.path.basename(save_path)}")
        print(f"⚠️  歌词无效（无时间戳），跳过下载：{os.path.basename(save_path)}")
        return False
    
    # 确认下载意愿（临时选择优先于配置）
    if temp_opinion not in ["y", "n"]:
        temp_opinion = GLOBAL_CONFIG["lrc_default"]
    
    if temp_opinion.lower() != "y":
        logger.info(f"未选择下载歌词：{os.path.basename(save_path)}")
        print(f"⏭️  未下载歌词：{os.path.basename(save_path)}（按配置/选择）")
        return False
    
    # 保存歌词
    save_dir = os.path.dirname(save_path)
    os.makedirs(save_dir, exist_ok=True)
    
    try:
        with open(save_path, 'w', encoding='UTF-8') as f:
            f.write(lrc_text)
        logger.info(f"歌词下载成功：{save_path}")
        print(f"✅ 歌词保存：{save_path}")
        return True
    except Exception as e:
        logger.error(f"歌词保存失败：{os.path.basename(save_path)}，错误：{str(e)}", exc_info=True)
        print(f"❌ 歌词保存失败：{str(e)}")
        return False


# -------------------------- 批量下载与报告 --------------------------
def single_song_task(song_info):
    """单首歌曲完整任务（供多线程调用）"""
    """song_info结构：{title, artist, download_url, song_path, lyrics_path, lrc_opinion, lyrics}"""
    result = {
        "title": song_info["title"],
        "artist": song_info["artist"],
        "song_path": song_info["song_path"],
        "lyrics_path": song_info["lyrics_path"],
        "download_status": "失败",
        "lrc_status": "未下载"
    }
    
    try:
        # 下载歌曲
        download_success, final_song_path = download_single_song(
            song_info["download_url"],
            song_info["song_path"]
        )
        result["song_path"] = final_song_path
        if download_success:
            result["download_status"] = "成功"
            # 下载歌词（仅歌曲成功后执行）
            lrc_success = lrc_download(
                song_info["lyrics"],
                song_info["lyrics_path"],
                song_info["lrc_opinion"]
            )
            result["lrc_status"] = "成功" if lrc_success else "失败"
    
    except Exception as e:
        logger.error(f"单首歌曲任务异常（{song_info['title']}）：{str(e)}", exc_info=True)
        result["download_status"] = f"异常：{str(e)[:20]}"
    
    return result


def batch_download_songs(selected_songs):
    """多线程批量下载歌曲（修复变量名冲突：title→song_title）"""
    clear()
    title()
    print(f"🚀 开始批量下载（共 {len(selected_songs)} 首，最大线程数：{GLOBAL_CONFIG['max_threads']}）")
    print("="*60 + "\n")
    
    # 提交任务到线程池（循环变量名改为song_title，避免与title()函数冲突）
    futures = []
    with ThreadPoolExecutor(max_workers=GLOBAL_CONFIG["max_threads"]) as executor:
        for song in selected_songs:
            future = executor.submit(single_song_task, song)
            futures.append((future, song["title"]))  # 此处song["title"]是歌曲名，变量名无冲突
    
    # 收集结果（循环变量名改为song_title）
    download_reports = []
    for future, song_title in futures:
        try:
            report = future.result()
            download_reports.append(report)
            print(f"\n{'-'*40}")  # 分隔不同歌曲的输出
        except Exception as e:
            logger.error(f"获取任务结果异常（{song_title}）：{str(e)}", exc_info=True)
            download_reports.append({
                "title": song_title,
                "artist": "未知",
                "song_path": "",
                "lyrics_path": "",
                "download_status": f"任务异常：{str(e)[:20]}",
                "lrc_status": "未下载"
            })
    
    return download_reports


def print_download_report(reports):
    """打印并保存下载报告"""
    clear()
    title()
    print("📊 批量下载报告")
    print("="*60)
    
    # 统计汇总
    total = len(reports)
    success_count = sum(1 for r in reports if r["download_status"] == "成功")
    fail_count = total - success_count
    
    print(f"总歌曲数：{total}")
    print(f"成功下载：{success_count} 首")
    print(f"下载失败：{fail_count} 首")
    print("="*60 + "\n")
    
    # 详细列表
    print("详细结果：")
    for idx, report in enumerate(reports, 1):
        print(f"[{idx}] 歌曲：《{report['title']}》- {report['artist']}")
        print(f"    音频状态：{report['download_status']} | 路径：{report['song_path'] if report['song_path'] else '无'}")
        print(f"    歌词状态：{report['lrc_status']} | 路径：{report['lyrics_path'] if report['lyrics_path'] else '无'}")
        print()
    
    # 失败歌曲单独列出（便于重试）
    fail_songs = [r for r in reports if r["download_status"] not in ["成功", "未下载"]]
    if fail_songs:
        print("❌ 失败歌曲列表（建议重试）：")
        for r in fail_songs:
            print(f"    - 《{r['title']}》- {r['artist']} | 原因：{r['download_status']}")
        print()
    
    # 保存报告到TXT
    save_choice = input("是否将报告保存为TXT文件？(y/n)：").strip().lower()
    if save_choice != "y":
        print("\n报告未保存，按回车键继续...")
        input()
        return
    
    # 生成带时间戳的报告文件名
    report_filename = f"download_report_{time.strftime('%Y%m%d_%H%M%S')}.txt"
    report_path = os.path.normpath(os.path.join(".", report_filename))
    
    try:
        with open(report_path, "w", encoding="utf-8") as f:
            f.write(f"批量下载报告（生成时间：{time.strftime('%Y-%m-%d %H:%M:%S')}）\n")
            f.write(f"总歌曲数：{total} | 成功：{success_count} | 失败：{fail_count}\n")
            f.write("="*80 + "\n\n")
            
            for idx, report in enumerate(reports, 1):
                f.write(f"[{idx}] 歌曲：《{report['title']}》- {report['artist']}\n")
                f.write(f"    音频状态：{report['download_status']}\n")
                f.write(f"    音频路径：{report['song_path'] if report['song_path'] else '无'}\n")
                f.write(f"    歌词状态：{report['lrc_status']}\n")
                f.write(f"    歌词路径：{report['lyrics_path'] if report['lyrics_path'] else '无'}\n")
                f.write("\n")
            
            if fail_songs:
                f.write("失败歌曲列表：\n")
                for r in fail_songs:
                    f.write(f"    - 《{r['title']}》- {r['artist']} | 原因：{r['download_status']}\n")
        
        logger.info(f"下载报告已保存：{report_path}")
        print(f"\n✅ 报告已保存：{report_path}")
    except Exception as e:
        logger.error(f"报告保存失败：{str(e)}", exc_info=True)
        print(f"❌ 报告保存失败：{str(e)}")
    
    input("\n按回车键继续...")


# -------------------------- 工具函数 --------------------------
def clear():
    """清空控制台"""
    os.system('cls' if os.name == 'nt' else 'clear')


def restart_or_exit():
    """重启/退出选择（按回车重启，其他键退出）"""
    clear()
    title()
    print("\n操作已完成！")
    try:
        import msvcrt
        print("按回车键重新运行，按任意其他键退出...")
        key = msvcrt.getch()
        return key == b'\r'
    except ImportError:
        import tty, termios
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            print("按回车键重新运行，按任意其他键退出...")
            key = sys.stdin.read(1)
            return key == '\r'
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)


# -------------------------- 主逻辑 --------------------------
def main_loop():
    base_search_url = 'https://www.gequbao.com/s/'   
    while True:
        try:
            # 1. 获取歌曲列表
            song_names = get_song_names()
            if not song_names:
                print("❌ 未获取到有效歌曲名，将重新开始...")
                input("按回车键继续...")
                clear()
                continue
            
            # 2. 搜索并选择歌曲（收集待下载列表）
            selected_songs = []
            for song_name in song_names:
                clear()
                title()
                print(f"🔍 正在搜索：{song_name}")
                
                # 生成搜索URL并解析
                main_url = get_main_url(base_search_url, song_name)
                main_html = get_url_html(main_url)
                if not main_html:
                    print(f"❌ 无法获取《{song_name}》的搜索页面，跳过该歌曲")
                    input("按回车键继续...")
                    continue
                
                # 提取搜索结果区域
                main_parse = parse_html(main_html, 1)
                if not main_parse:
                    logger.warning(f"未找到《{song_name}》的搜索结果")
                    print(f"❌ 未找到《{song_name}》的搜索结果，跳过该歌曲")
                    input("按回车键继续...")
                    continue
                
                # 提取歌曲列表
                music_list = parse_html(main_parse, 2)
                if not music_list:
                    logger.warning(f"《{song_name}》搜索结果中无有效歌曲")
                    print(f"❌ 《{song_name}》搜索结果中无有效歌曲，跳过该歌曲")
                    input("按回车键继续...")
                    continue
                
                # 选择目标歌曲
                user_choose = choose_music(music_list, song_name)
                second_url, music_info = get_second_url(user_choose, music_list)
                
                # 获取歌曲详情页HTML
                second_html = get_url_html(second_url)
                if not second_html:
                    print(f"❌ 无法获取《{music_info['title']}》的详情页，跳过该歌曲")
                    input("按回车键继续...")
                    continue
                
                # 提取play_id和下载链接
                play_id = get_play_id(second_html)
                if not play_id:
                    print(f"❌ 无法提取《{music_info['title']}》的play_id，跳过该歌曲")
                    input("按回车键继续...")
                    continue
                
                download_json = get_download_json(play_id, second_url)
                download_url = get_download_url(download_json)
                if not download_url:
                    print(f"❌ 无法提取《{music_info['title']}》的下载链接，跳过该歌曲")
                    input("按回车键继续...")
                    continue
                
                # 提取并处理歌词
                lyrics_html = get_lyrics_html(second_html)
                lyrics_text = process_lrc_text(lyrics_html) if lyrics_html else ""
                
                # 临时选择是否下载歌词
                clear()
                title()
                print(f"🎵 已选中歌曲：《{music_info['title']}》- {music_info['artist']}")
                lrc_temp = input(f"是否下载该歌曲的歌词？(y/n，默认按配置：{GLOBAL_CONFIG['lrc_default']})：").strip().lower()
                
                # 生成保存路径
                song_save_path, lyrics_save_path = get_save_path(
                    music_info["title"],
                    music_info["artist"]
                )
                
                # 添加到待下载列表
                selected_songs.append({
                    "title": music_info["title"],
                    "artist": music_info["artist"],
                    "music_id": music_info["music_id"],
                    "detail_url": second_url,
                    "download_url": download_url,
                    "song_path": song_save_path,
                    "lyrics_path": lyrics_save_path,
                    "lrc_opinion": lrc_temp,
                    "lyrics": lyrics_text
                })
                
                logger.info(f"成功添加待下载歌曲：《{music_info['title']}》- {music_info['artist']}")
                print(f"\n✅ 已添加到待下载列表，共 {len(selected_songs)} 首歌曲")
                input("按回车键继续选择下一首...")
            
            # 3. 确认并批量下载
            if not selected_songs:
                print("❌ 未选中任何有效歌曲，将重新开始...")
                input("按回车键继续...")
                clear()
                continue
            
            # 显示待下载列表
            clear()
            title()
            print(f"📋 待下载歌曲列表（共 {len(selected_songs)} 首）：")
            for idx, song in enumerate(selected_songs, 1):
                print(f"[{idx}] 《{song['title']}》- {song['artist']} | 详情页：{song['detail_url']}")
            
            # 确认下载
            while True:
                opinion = input("\n是否开始批量下载？(y/n)：").strip().lower()
                if opinion in ["y", "n"]:
                    break
                print("❌ 输入错误，请输入y（下载）或n（取消）！")
            
            if opinion == "y":
                # 开始多线程下载
                download_reports = batch_download_songs(selected_songs)
                # 显示下载报告
                print_download_report(download_reports)
            else:
                logger.info("用户取消批量下载")
                print("❌ 已取消批量下载")
                input("按回车键继续...")
        
        except Exception as e:
            logger.error(f"程序主逻辑异常：{str(e)}", exc_info=True)
            print(f"\n❌ 程序发生错误：{str(e)}")
            print("详细错误信息已保存到 music_downloader.log")
            traceback.print_exc()
            input("按回车键继续...")
        
        # 4. 重启或退出
        if not restart_or_exit():
            logger.info("用户选择退出程序")
            print("👋 程序已退出，感谢使用！")
            break
        clear()


if __name__ == "__main__":
    # 检查依赖库
    required_libs = ["requests", "bs4", "tqdm", "tenacity"]
    missing_libs = []
    for lib in required_libs:
        try:
            __import__(lib)
        except ImportError:
            missing_libs.append(lib)
    
    if missing_libs:
        print(f"❌ 检测到缺失依赖库：{', '.join(missing_libs)}")
        print(f"请先执行安装命令：pip install {' '.join(missing_libs)}")
        sys.exit(1)
    
    # 启动主循环
    main_loop()